﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public class SubFetchMapping<TDatabase, TParentDbEntity, TChildDbEntity, TParentOutput, TChildOutput, TPrimaryKey, TOutputValue> : IMapping<TDatabase>
        where TDatabase : IDisposable
        where TChildOutput : class
        where TOutputValue : class
    {
        private Func<TDatabase, IEnumerable<TPrimaryKey>, IQueryable<TChildDbEntity>> fetcher;
        private Mapper<TDatabase, TChildDbEntity, TChildOutput> mapper;
        private IDbValueMapping<TDatabase, TParentDbEntity, TPrimaryKey> primaryKey;
        private IDbValueMapping<TDatabase, TChildDbEntity, TPrimaryKey> foreignKey;
        private Expression<Func<TParentOutput, TOutputValue>> outputSelector;
        
        private PropertyInfo property;
        private MethodInfo strategyCast;
        private MethodInfo strategyToArray;

        public SubFetchMapping(IDbValueMapping<TDatabase, TParentDbEntity, TPrimaryKey> primaryKey, IDbValueMapping<TDatabase, TChildDbEntity, TPrimaryKey> foreignKey, Func<TDatabase, IEnumerable<TPrimaryKey>, IQueryable<TChildDbEntity>> fetcher, Mapper<TDatabase, TChildDbEntity, TChildOutput> mapper, Expression<Func<TParentOutput, TOutputValue>> outputSelector)
        {
            this.primaryKey = primaryKey;
            this.foreignKey = foreignKey;
            this.fetcher = fetcher;
            this.mapper = mapper;
            this.outputSelector = outputSelector;
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            mapper.Initialize();

            var propertyName = NameHelpers.SuggestName(outputSelector);
            property = type.DefineProperty(propertyName, mapper.TransientType.MakeArrayType());
        }

        public void DefineTransientInitialization(MapContext<TDatabase> mapContext, Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            property = transientType.GetProperty(property.Name);

            strategyCast = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(mapper.TransientType); 
            strategyToArray = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(mapper.TransientType); 
        }

        void IMapping<TDatabase>.OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
            OnTransientInitialized(mapContext, transient);
        }

        private void OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
            var enumerable = (IEnumerable)transient;
            var pks = primaryKey.Select(enumerable);

            Array subTransient;
            using (var dbInstance = mapContext.DbAdapter.CreateDatabase())
            {
                var db = dbInstance.Db;
                var dbResult = fetcher(db, pks);
                
                // Copy value from result to the parent transient
                subTransient = mapper.TransformToTransient(mapContext, db, dbResult);
            }
            mapper.OnTransientInitialized(mapContext, subTransient);

            // Split into dictionary
            var dictionary = foreignKey.GroupBy(subTransient).ToDictionary(x => x.Key, x => x.ToArray());

            // Now weave the results of the dictionary back into the parent result
            foreach (var o in enumerable)
            {
                var parentId = primaryKey.Get(o);
                object[] items;
                if (!dictionary.TryGetValue(parentId, out items))
                    items = new object[0];

                var casted = strategyCast.Invoke(null, new[] { items });
                var array = strategyToArray.Invoke(null, new[] { casted });

                property.SetValue(o, array, null);
            }
        }

        public void MapToOutput(MapContext<TDatabase> mapContext, object transient, object output)
        {
            var subInput = (Array)property.GetValue(transient, null);

            var subValue = mapper.TransformToOutput(mapContext, subInput);
            object value = subValue;

            var outputProperty = outputSelector.GetPropertyInfo();
            if (outputProperty.PropertyType.IsGenericList())
                value = subValue.ToList();

            outputProperty.SetValue(output, value, null);
        }

        public void MapToInput(MapContext<TDatabase> mapContext, object output, object input)
        {
        }

        public override string ToString()
        {
            return string.Format("SubFetchMapping({0}, {1}, {2})", primaryKey, foreignKey, outputSelector);
        }
    }
}